<template>
    <div class='editor-container' :style="hex_mode ? 'display: inline-flex' : ''">
        <el-container :style="hex_mode ? 'width: 50%;min-width: 600px;' : ''">
            <el-header>
                <el-row :gutter="0">
                    <el-col :span="1">
                        <el-icon-Edit width="25" />
                    </el-col>
                    <el-col v-if="!hex_mode" :span="2" style="padding: 0 5px;">
                        <el-select-v2 v-model="curSelect" :options="selectLanguage" placeholder="请选择语言" />
                    </el-col>
                    <el-col :span="!hex_mode ? 12 : 14" :offset="3">
                        {{ name }}
                    </el-col>
                    <el-col :span="2" :offset="4" style="justify-content: flex-end;">
                        <el-tooltip v-if="!hex_mode" effect="dark" content="保存" placement="bottom">
                            <el-button type="success" :icon="CopyDocument" circle plain :disabled="!ischange"
                                @click="emits('save', codeValue)" />
                        </el-tooltip>
                        <el-tooltip effect="dark" content="关闭" placement="bottom">
                            <el-button type="danger" :icon="Close" circle plain
                                @click="emits('close', stringToUint8Array(codeValue))" />
                        </el-tooltip>
                    </el-col>
                </el-row></el-header>
            <el-main>
                <div ref="cdRef" style="height: 100%;"></div>
            </el-main>
        </el-container>
    </div>
</template>
<script setup lang="ts">
import { CopyDocument, Close } from '@element-plus/icons-vue'

const props = defineProps({
    contents: {
        type: Uint8Array,
        default: new Uint8Array(0)
    },
    title: {
        type: String,
        default: '测试标题'
    },
    height: {
        type: String,
        default: '400px'
    },
    hex_mode: {
        type: Boolean,
        default: false
    },
})
const emits = defineEmits(['save', "close"])
const name = toRef(props.title)
function buf2hex(buffer) {
    return Array.prototype.map.call(buffer, x => (
        x.toString(16)).toUpperCase().slice(-2).padStart(2, '0')).join(' ');
}
function buf2str(buffer) {
    return Array.prototype.map.call(buffer, x => (
        (31 < x && x < 127) ? String.fromCharCode(x) : '.')).join("")
}

const hex_data = computed(() => {
    let res = ""
    // var result = [];
    if (props.contents.length) {
        res += "00 01 02 03  04 05 06 07  08 09 0A 0B  0C 0D 0E 0F                0123456789ABCDEF\n"
        for (var i = 0, len = props.contents.length; i < len; i += 16) {
            let source = props.contents.slice(i, i + 16);
            let str = buf2str(source)
            let item = buf2hex(source).padEnd(16 * 3 + 2, ' ')
            for (let j = 12; j < item.length; j += 13) {
                item = item.slice(0, j) + " " + item.slice(j)
            }
            if (i)
                res += '\n'
            res += item
            res += "            "
            res += str
        }
    }
    return res
})

function Uint8ArrayToString(arr: Uint8Array) {
    var dataString = "";
    for (var i = 0; i < arr.length; i++) {
        dataString += String.fromCharCode(arr[i]);
    }

    return dataString
}

function stringToUint8Array(str: String) {
    var arr = [];
    for (var i = 0, j = str.length; i < j; ++i) {
        arr.push(str.charCodeAt(i));
    }

    var tmpUint8Array = new Uint8Array(arr);
    return tmpUint8Array
}

import { EditorView } from "@codemirror/view";
import { insertTab } from "@codemirror/commands";
import { EditorState } from '@codemirror/state';

import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap } from '@codemirror/view';
import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';

/* language */
import { css } from "@codemirror/lang-css"
import { less } from "@codemirror/lang-less"
import { sass } from "@codemirror/lang-sass"
import { html } from "@codemirror/lang-html"
import { xml } from "@codemirror/lang-xml"
import { javascript } from "@codemirror/lang-javascript";
import { yaml } from "@codemirror/lang-yaml";
import { python } from "@codemirror/lang-python";
import { markdown } from "@codemirror/lang-markdown";
import { vue } from "@codemirror/lang-vue";
import { rust } from "@codemirror/lang-rust";
import { cpp } from "@codemirror/lang-cpp";
import { json } from "@codemirror/lang-json";
const selectLanguage = [
    { label: 'HTML', value: 'html' },
    { label: 'XML', value: 'xml' },
    { label: 'CSS', value: 'css' },
    { label: 'SASS', value: 'sass' },
    { label: 'LESS', value: 'less' },
    { label: 'JavaScript', value: 'javascript' },
    { label: 'YAML', value: 'yaml' },
    { label: 'JSON', value: 'json' },
    { label: 'Markdown', value: 'markdown' },
    { label: 'Python', value: 'python' },
    { label: 'Rust', value: 'rust' },
    { label: 'Vue', value: 'vue' },
    { label: 'C/C++', value: 'cpp' },
]

const type2language = [
    { type: ['.html', '.htm', '.xhtml'], language: 'html' },
    { type: ['.xml'], language: 'xml' },
    { type: ['.css'], language: 'css' },
    { type: ['.sass'], language: 'sass' },
    { type: ['.less'], language: 'less' },
    { type: ['.js', '.ts'], language: 'javascript' },
    { type: ['.yml', '.yaml'], language: 'yaml' },
    { type: ['.json'], language: 'json' },
    { type: ['.md'], language: 'markdown' },
    { type: ['.py'], language: 'python' },
    { type: ['.rs'], language: 'rust' },
    { type: ['.vue'], language: 'vue' },
    { type: ['.c', '.cpp'], language: 'cpp' },
];

const LanguageFunction = [
    { name: 'html', language: html() },
    { name: 'xml', language: xml() },
    { name: 'css', language: css() },
    { name: 'sass', language: sass() },
    { name: 'less', language: less() },
    { name: 'javascript', language: javascript() },
    { name: 'yaml', language: yaml() },
    { name: 'json', language: json() },
    { name: 'markdown', language: markdown() },
    { name: 'python', language: python() },
    { name: 'rust', language: rust },
    { name: 'vue', language: vue() },
    { name: 'cpp', language: cpp() },
]

const curSelect = ref('javascript');
const cdRef = ref();
const codeValue = ref(props.hex_mode ? hex_data : Uint8ArrayToString(props.contents));
const cdView = ref<EditorView>();
const fixedHeightEditor = EditorView.theme({
    "&": {
        height: "100%",
        // color: "white",
        // backgroundColor: "#034"
    },
    ".cm-content": {
        caretColor: props.hex_mode ? "white" : "",
        fontSize: props.hex_mode ? "1.2em" : "1.1em",
        paddingRight: "10px"
    },
    "&.cm-focused .cm-cursor": {
        // borderLeftColor: "#0e9"
    },
    "&.cm-focused .cm-selectionBackground ::selection": {
        backgroundColor: "#00000030"
    },
    "&.cm-focused .cm-selectionBackground ::-moz-selection": {
        backgroundColor: "#00000030"
    },
    /* 当前选中行 */
    ".cm-activeLine": {
        backgroundColor: "#00000026"
    },
    ".cm-tooltip": {
        border: "none",
        borderRadius: "0px"
    },
    /* 侧边数字 */
    ".cm-gutters": {
        backgroundColor: "#045",
        color: "#ddd",
        border: "none",
        fontSize: props.hex_mode ? "1.2em" : "1.1em"
    },
    ".cm-gutter": {
        marginRight: "5px",
    },
    ".cm-activeLineGutter": {
        backgroundColor: "#045"
    },
})

const ischange = computed(() => {
    return props.contents != codeValue.value
})

for (let item of type2language) {
    if (item.type.indexOf(props.title) != -1) {
        curSelect.value = item.language
    }
}

const language = () => {
    for (let item of LanguageFunction) {
        if (item.name == curSelect.value) {
            return item.language
        }
    }
    return javascript()
}

const hexLineNumbers = lineNumbers({
    formatNumber: n => {
        if (n < 2)
            return ''
        else if (n == 2)
            return '00000000'
        let res = ((n - 2) * 16).toString(16).toUpperCase()
        return res.padStart(8, '0')
    }
})

const externs = computed(() => {
    if (props.hex_mode)
        return [
            fixedHeightEditor,
            hexLineNumbers,
            EditorState.readOnly.of(true),
        ]
    return [
        language(),
        fixedHeightEditor,
        EditorView.lineWrapping,// 自动换行
        keymap.of([
            ...defaultKeymap,
            {
                key: 'Tab',
                run: insertTab,
            },
        ]),
        EditorView.updateListener.of((v) => {
            codeValue.value = v.state.doc.toString() // 监测得到的最新内容并更新到监听的容器中 
        }),
        /* baseSetup */
        lineNumbers(),
        highlightActiveLineGutter(),
        highlightSpecialChars(),
        history(),
        foldGutter(),
        drawSelection(),
        dropCursor(),
        EditorState.allowMultipleSelections.of(true),
        indentOnInput(),
        syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
        bracketMatching(),
        closeBrackets(),
        autocompletion(),
        rectangularSelection(),
        crosshairCursor(),
        highlightActiveLine(),
        highlightSelectionMatches(),
        keymap.of([
            ...closeBracketsKeymap,
            ...defaultKeymap,
            ...searchKeymap,
            ...historyKeymap,
            ...foldKeymap,
            ...completionKeymap,
            ...lintKeymap
        ]),]
})

// 赋值编辑器的方法，用于作为公共组件时，由父级组件触发编辑器内容的更新
const setInput = (value) => {
    if (!cdView.value) return;
    cdView.value.dispatch({ changes: { from: 0, to: cdView.value.state.doc.length, insert: value } });
};
const initEditor = () => {
    if (cdView.value) {
        cdView.value.destroy();
        cdView.value = null;
    }
    const state = EditorState.create({
        doc: codeValue.value,
        extensions: externs.value
    });
    cdView.value = new EditorView({
        state,
        parent: cdRef.value
    });
}

watch(curSelect, () => {
    initEditor()
})

onMounted(() => {
    initEditor()
});

onBeforeUnmount(() => {
    if (cdView.value) {
        cdView.value.destroy();
        cdView.value = null;
    }
})

defineExpose({
    setInput
});
</script>
<style scoped>
.editor-container {
    height: 100%;
    display: -webkit-box
}

:deep(.el-container) {
    width: 95%;
    height: 95%;
    text-align: start;
    box-shadow: #00000099 1px 1px 10px;
    padding: 0;
    border-radius: 10px;
    margin: auto;
}

:deep(.el-header) {
    height: auto;
    padding: 5px;
    border-bottom: solid 1px #0000003b;
}

:deep(.el-main) {
    padding: 0;
}

.el-row {
    margin: 0;
}

:deep(.cm-scroller) {
    overflow: "auto";
    scrollbar-width: thin;
    scrollbar-color: #888 #f1f1f1;
}

:deep(.cm-scroller)::-webkit-scrollbar {
    width: 6px;
    height: 1px;
}

:deep(.cm-scroller)::-webkit-scrollbar-track {
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    border-radius: 10px;
    background: #EDEDED;
}

:deep(.cm-scroller)::-webkit-scrollbar-thumb {
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    background: #535353;
}

.hexdump {
    height: auto;
    font-size: 0.9em;
}

.hexdump__item {
    display: inline-block;
    font-family: 'Courier New', Courier, 'Lucida Console', Consolas, monospace;
}

:deep(.resize-observer) {
    display: none;
}
</style>